﻿#region License
/* Copyright (C) 2009 Tim Coulter
 * 
 * This file is part of ExtremeML.
 * 
 * ExtremeML is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation either version
 * 2 of the License, or (at your option) any later version.
 * 
 * ExtremeML is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;

namespace ExtremeML.Builder
{
	/// <summary>
	/// A static class that exposes lookup methods that aid the interpretation of SDK type metadata.
	/// </summary>
	internal static class Lookup
	{
		#region Declarations
		private const string ChildElementReferenceResource = @"ExtremeML.Builder.Data.ChildElementReference.xml";
		private const string ExplicitPatternsResource = @"ExtremeML.Builder.Data.ExplicitPatterns.xml";
		private const string UnsupportedClassesResource = @"ExtremeML.Builder.Data.UnsupportedClasses.xml";

		private const string FromFrameworkNullableExpressionTemplate = "{{0}}.HasValue ? {{0}}.To{0}() : null";
		private const string FromFrameworkStringExpressionTemplate = "{{0}} == null ? null : {0}.FromString({{0}})";
		private const string ToFrameworkNullableExpressionTemplate = "{0} == null ? null : {0}.ToNullable()";
		private const string ToFrameworkStringExpressionTemplate = "{0} == null ? null : {0}.ToString()";

		private static bool _filterSupportedParts = true;
		private static List<Type> _supportedPartTypes = GetSupportedPartTypes();
		private static Dictionary<Type, IEnumerable<Type>> _childElementCollections = GetChildElementCollectionTable();
		private static Dictionary<Type, Dictionary<Type, string>> _childElementProperties = GetChildElementPropertyTable();
		private static Dictionary<Type, Type> _scalarTypes = GetScalarTypesTable();
		private static Dictionary<Type, SimpleTypeMapping> _simpleTypeMappings = GetSimpleTypeMappingsTable();
		private static Dictionary<string, string> _namePlurals = GetNamePlurals();
		private static Dictionary<string, Dictionary<string, CustomPattern>> _customPatterns = GetExplicitPatternsTable();
		private static List<Type> _unsupportedClasses = GetUnsupportedClassesTable();

		#endregion
		#region Type Tests

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is one of the pre-defined scalar types.
		/// </summary>
		public static bool IsScalarType(
			Type type)
		{
			return type != null && _scalarTypes.ContainsKey(type);
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is one of the
		/// pre-defined SDK types that support SimpleType Xml attribute data.
		/// </summary>
		public static bool IsSimpleSdkType(
			Type type)
		{
			return type != null && _simpleTypeMappings.ContainsKey(type);
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is one of the
		/// pre-defined SDK enumeration types that use EnumValue/>.
		/// </summary>
		public static bool IsEnumSdkType(
			Type type)
		{
			return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(EnumValue<>);
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is derived from <see cref="OpenXmlPart"/>.
		/// </summary>
		public static bool IsPartType(
			Type type)
		{
			if (_filterSupportedParts)
			{
				return type != null && _supportedPartTypes.Contains(type);
			}
			else
			{
				return type != null && !type.IsAbstract && type.IsSubclassOf(typeof(OpenXmlPart));
			}
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is a set
		/// of a class derived from <see cref="OpenXmlPart"/>.
		/// </summary>
		public static bool IsPartCollectionType(
			Type type)
		{
			return type != null && type.IsGenericType &&
				type.GetGenericTypeDefinition() == typeof(IEnumerable<>) &&
				IsPartType(type.GetGenericArguments()[0]);
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is derived from <see cref="OpenXmlPartRootElement"/>.
		/// </summary>
		public static bool IsRootElementType(
			Type type)
		{
			return type != null && !type.IsAbstract && type.IsSubclassOf(typeof(OpenXmlPartRootElement));
		}
	
		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is derived from <see cref="OpenXmlElement"/>.
		/// </summary>
		public static bool IsElementType(
			Type type)
		{
			return type != null && !type.IsAbstract && type.IsSubclassOf(typeof(OpenXmlElement));
		}

		/// <summary>
		/// Returns whether the passed <see cref="Type"/> is one of the explcicitly configured unsupported types.
		/// </summary>
		public static bool IsUnsupportedType(
			Type type)
		{
			if (_unsupportedClasses.Contains(type))
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		#endregion
		#region Type Lookup

		/// <summary>
		/// Extracts the first generic type argument from from the passed generic <see cref="Type"/>.
		/// </summary>
		public static Type GetGenericContainedType(
			Type type)
		{
			return type.IsGenericType ? type.GetGenericArguments()[0] : null;
		}

		/// <summary>
		/// Returns the expanded name of the passed <see cref="Type"/> in C# format.
		/// </summary>
		public static string GetGenericExpandedName(
			Type type)
		{
			if (type.IsGenericType)
			{
				var result = string.Empty;

				foreach (var argument in type.GetGenericArguments())
				{
					if (result != string.Empty) result += ", ";
					result += GetCSharpTypeName(argument);
				}

				return type.Name.Substring(0, type.Name.IndexOf('`')) + "<" + result + ">";
			}
			else
			{
				return GetCSharpTypeName(type);
			}
		}

		/// <summary>
		/// Returns the C# internal name for the passed <see cref="Type"/>, defaulting to the CLR name.
		/// </summary>
		public static string GetCSharpTypeName(
			Type type)
		{
			switch (type.FullName)
			{
				case "System.String" : return "string";
				case "System.Boolean": return "bool";
				case "System.Decimal": return "decimal";
				case "System.Int32": return "int";
				case "System.UInt32": return "uint";
				case "System.Byte": return "byte";
				case "System.Double": return "double";
				case "System.Char": return "char";
				case "System.Single": return "float";
				case "System.Int64": return "long";
				case "System.Object": return "object";
				case "System.SByte": return "sbyte";
				case "System.UInt16": return "ushort";
				case "System.UInt64": return "ulong";
				case "System.Int16": return "short";
				default: return type.FullName;
			}
		}

		/// <summary>
		/// Returns the <see cref="SimpleTypeMapping"/> that corresponds to the passed SDK <see cref="Type"/>.
		/// </summary>
		public static SimpleTypeMapping GetSimpleTypeMapping(
			Type type)
		{
			if (_simpleTypeMappings.ContainsKey(type))
			{
				return _simpleTypeMappings[type];
			}
			else
			{
				throw new NotSupportedException("Unsupported SDK type");
			}
		}

		/// <summary>
		/// Returns the framework <see cref="Type"/> that corresponds to the passed SDK EnumValue type.
		/// </summary>
		public static Type GetEnumFrameworkType(
			Type type)
		{
			return typeof(Nullable<>).MakeGenericType(new[] { GetGenericContainedType(type) });
		}

		/// <summary>
		/// Returns the plural form of the passed name.
		/// </summary>
		public static string GetPluralName(
			string name)
		{
			foreach (var plural in _namePlurals)
			{
				if (name.EndsWith(plural.Key))
				{
					return name.Substring(0, name.Length - plural.Key.Length) + plural.Value;
				}
			}

			return name + "s";
		}

		#endregion
		#region MetaData Retrieval

		/// <summary>
		/// Returns the set of types that are exposed as child element
		/// collection properties on the passed parent <see cref="Type"/>
		/// </summary>
		public static IEnumerable<Type> GetChildElementCollectionTypes(
			Type type)
		{
			return _childElementCollections.ContainsKey(type) ? _childElementCollections[type] : new List<Type>();
		}

		/// <summary>
		/// Returns the set of types that are exposed as child element
		/// collection properties on the passed parent <see cref="Type"/>
		/// </summary>
		public static Dictionary<Type, string> GetChildElementPropertyTypes(
			Type type)
		{
			return _childElementProperties.ContainsKey(type) ? _childElementProperties[type] : new Dictionary<Type, string>();
		}

		/// <summary>
		/// Returns a dictionary of custom patterns explicitly defined for the wrapper class of the specified name.
		/// </summary>
		public static Dictionary<string, CustomPattern> GetExplicitPatterns(
			string typeName)
		{
			return _customPatterns.ContainsKey(typeName) ? _customPatterns[typeName] : new Dictionary<string, CustomPattern>();
		}

		/// <summary>
		/// Returns a dictionary of irregular singular class name endings and their plural forms.
		/// </summary>
		private static Dictionary<string, string> GetNamePlurals()
		{
			return new Dictionary<string, string>
			{
				{"data", "data"},
				{"Data", "Data"},
				{"Mdx", "Mdxes"},
				{"Info", "Info"},
				{"Query", "Queries"},
				{"Entry", "Entries"},
				{"Category", "Categories"},
				{"Categories", "CategorySets"},
				{"Hierarchy", "Hierarchies"},
				{"Property", "Properties"},
				{"Properties", "PropertySets"},
				{"Columns", "ColumnSets"}
			};
		}

		#endregion
		#region Initialization

		/// <summary>
		/// Returns a dictionary of types representing the specific framework types that we classify as scalar.
		/// </summary>
		private static Dictionary<Type, Type> GetScalarTypesTable()
		{
			return new Dictionary<Type, Type>
			{
				{typeof(string), null},
				{typeof(Uri), null}
			};
		}

		/// <summary>
		/// Returns a dictionary of types representing the mappings between SDk simple types and framework types.
		/// </summary>
		private static Dictionary<Type, SimpleTypeMapping> GetSimpleTypeMappingsTable()
		{
			return new Dictionary<Type, SimpleTypeMapping>
			{
				{
					typeof(BooleanValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(BooleanValue),
						FrameworkType = typeof(bool?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(BooleanValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(ByteValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(ByteValue),
						FrameworkType = typeof(byte?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(ByteValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(DateTimeValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(DateTimeValue),
						FrameworkType = typeof(DateTime?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(DateTimeValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(DecimalValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(DecimalValue),
						FrameworkType = typeof(decimal?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(DecimalValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(DoubleValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(DoubleValue),
						FrameworkType = typeof(double?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(DoubleValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(Int16Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(Int16Value),
						FrameworkType = typeof(int?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(Int16Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(Int32Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(Int32Value),
						FrameworkType = typeof(int?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(Int32Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(Int64Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(Int64Value),
						FrameworkType = typeof(int?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(Int64Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(IntegerValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(IntegerValue),
						FrameworkType = typeof(int?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(IntegerValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(OnOffValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(OnOffValue),
						FrameworkType = typeof(bool?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(OnOffValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(SByteValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(SByteValue),
						FrameworkType = typeof(sbyte?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(SByteValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(SingleValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(SingleValue),
						FrameworkType = typeof(float?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(SingleValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(TrueFalseValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(TrueFalseValue),
						FrameworkType = typeof(bool?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(TrueFalseValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(TrueFalseBlankValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(TrueFalseBlankValue),
						FrameworkType = typeof(bool?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(TrueFalseBlankValue).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(UInt16Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(UInt16Value),
						FrameworkType = typeof(uint?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(UInt16Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(UInt32Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(UInt32Value),
						FrameworkType = typeof(uint?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(UInt32Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(UInt64Value),
					new SimpleTypeMapping
					{
						SdkType = typeof(UInt64Value),
						FrameworkType = typeof(uint?),
						ToSdkTypeExpression = string.Format(FromFrameworkNullableExpressionTemplate, typeof(UInt64Value).Name),
						ToFrameworkTypeExpression = ToFrameworkNullableExpressionTemplate
					}
				},
				{
					typeof(Base64BinaryValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(Base64BinaryValue),
						FrameworkType = typeof(string),
						ToSdkTypeExpression = string.Format(FromFrameworkStringExpressionTemplate, typeof(Base64BinaryValue).Name),
						ToFrameworkTypeExpression = ToFrameworkStringExpressionTemplate
					}
				},
				{
					typeof(HexBinaryValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(HexBinaryValue),
						FrameworkType = typeof(string),
						ToSdkTypeExpression = string.Format(FromFrameworkStringExpressionTemplate, typeof(HexBinaryValue).Name),
						ToFrameworkTypeExpression = ToFrameworkStringExpressionTemplate
					}
				},
				{
					typeof(StringValue),
					new SimpleTypeMapping
					{
						SdkType = typeof(StringValue),
						FrameworkType = typeof(string),
						ToSdkTypeExpression = string.Format(FromFrameworkStringExpressionTemplate, typeof(StringValue).Name),
						ToFrameworkTypeExpression = ToFrameworkStringExpressionTemplate
					}
				}
			};
		}

		/// <summary>
		/// Returns the list of currently supported package part types.
		/// </summary>
		/// <returns></returns>
		private static List<Type> GetSupportedPartTypes()
		{
			return new List<Type>
			{
				typeof(WorkbookPart),
					typeof(CalculationChainPart),
					typeof(SharedStringTablePart),
					typeof(ThemePart),
						typeof(ImagePart),
					typeof(WorkbookStylesPart),
					typeof(WorksheetPart),
						typeof(TableDefinitionPart),
							typeof(QueryTablePart),
						typeof(WorksheetCommentsPart),
					typeof(VmlDrawingPart)
			};
		}

		/// <summary>
		/// Returns a dictionary of sets representing the multiple
		/// instance child element types applicable to each element type. 
		/// </summary>
		private static Dictionary<Type, IEnumerable<Type>> GetChildElementCollectionTable()
		{
			var document = GetChildElementReferenceXml();
			var result = new Dictionary<Type, IEnumerable<Type>>();
			var assembly = Assembly.GetAssembly(typeof(OpenXmlElement));

			foreach (var type in document.XPathSelectElements("//classes/class[.//multipleInstanceChildElements/child]"))
			{
				result.Add(
					assembly.GetType(type.Attribute("name").Value),
					new List<Type>(from child in type.XPathSelectElements(".//multipleInstanceChildElements/child")
								   select assembly.GetType(child.Attribute("name").Value)));
			}

			return result;
		}

		/// <summary>
		/// Returns a dictionary of dictionaries representing the single
		/// instance child element types applicable to each element type and,
		/// where applicable, the property of the parent type that expose them.
		/// </summary>
		private static Dictionary<Type, Dictionary<Type, string>> GetChildElementPropertyTable()
		{
			var document = GetChildElementReferenceXml();
			var result = new Dictionary<Type, Dictionary<Type, string>>();
			var assembly = Assembly.GetAssembly(typeof(OpenXmlElement));

			foreach (var type in document.XPathSelectElements("//classes/class[.//singleInstanceChildElements/child]"))
			{
				var dictonary = new Dictionary<Type, string>();
				result.Add(assembly.GetType(type.Attribute("name").Value), dictonary);

				foreach (var child in type.XPathSelectElements(".//singleInstanceChildElements/child"))
				{
					dictonary.Add(
						assembly.GetType(child.Attribute("name").Value),
						child.Attribute("property") == null ? null : child.Attribute("property").Value);
				}
			}

			return result;
		}

		/// <summary>
		/// Returns a list of <see cref="Type"/> representing the SDK types that have been
		/// explcicitly configured as unsupported (for reasons specified in the Xml file).
		/// </summary>
		private static List<Type> GetUnsupportedClassesTable()
		{
			var document = GetUnsupportedClassesXml();
			var result = new List<Type>();
			var assembly = Assembly.GetAssembly(typeof(OpenXmlElement));

			foreach (var type in document.XPathSelectElements("//classes/class"))
			{
				result.Add(assembly.GetType(type.Attribute("name").Value));
			}

			return result;
		}

		/// <summary>
		/// Returns a dictionary of dictionaries representing the patterns explicitly defined for wrapper classes.
		/// </summary>
		private static Dictionary<string, Dictionary<string, CustomPattern>> GetExplicitPatternsTable()
		{
			var document = GetExplicitPatternsXml();
			var result = new Dictionary<string, Dictionary<string, CustomPattern>>();

			foreach (var wrapper in document.XPathSelectElements("//wrappers/wrapper"))
			{
				var patterns = new Dictionary<string, CustomPattern>();
				result.Add(wrapper.Attribute("name").Value, patterns);

				foreach (var pattern in wrapper.XPathSelectElements(".//pattern"))
				{
					var custom = new CustomPattern(pattern.Attribute("name").Value);
					patterns.Add(custom.Name, custom);

					if (pattern.Attribute("eventListeners") != null)
					{
						custom.HasEventListeners = bool.Parse(pattern.Attribute("eventListeners").Value);
					}
		
					foreach (var argument in pattern.XPathSelectElements(".//argument"))
					{
						custom.Arguments.Add(argument.Attribute("name").Value, argument.Attribute("value").Value);
					}
				}
			}

			return result;
		}

		/// <summary>
		/// Returns an <see cref="XDocument"/> that contains the surveyed the child element reference data.
		/// </summary>
		private static XDocument GetChildElementReferenceXml()
		{
			using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ChildElementReferenceResource))
			using (var reader = XmlReader.Create(stream))
			{
				return XDocument.Load(reader);
			}
		}

		/// <summary>
		/// Returns an <see cref="XDocument"/> that contains the surveyed the child element reference data.
		/// </summary>
		private static XDocument GetExplicitPatternsXml()
		{
			using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ExplicitPatternsResource))
			using (var reader = XmlReader.Create(stream))
			{
				return XDocument.Load(reader);
			}
		}

		/// <summary>
		/// Returns an <see cref="XDocument"/> that contains the list of unsupported SDK types.
		/// </summary>
		private static XDocument GetUnsupportedClassesXml()
		{
			using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(UnsupportedClassesResource))
			using (var reader = XmlReader.Create(stream))
			{
				return XDocument.Load(reader);
			}
		}

		#endregion
	}
}